# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import os
import pickle
import pandas as pd
import numpy as np
from joblib import Parallel, delayed
from config import curve_path, utc_offset, data_center_path, data_path, njob, error_webhook_url
from utils.dingding import send_wechat_work_msg
import warnings
warnings.filterwarnings('ignore')


def calc_curve(select_coin, cls, run_time, _offset, last_offset_list):
    """
    计算资金曲线
    如果本地存在选币文件&资金曲线，那么只需要计算最近2个offset数据
    :param select_coin: 选币文件
    :param cls: 子策略配置信息
    :param run_time: 当前运行时间
    :param _offset: offset
    :param last_offset_list: 最新一个周期的2个offset数据
    :return:
    """
    # ===简单的处理数据
    period = cls.hold_period
    select_coin = select_coin[select_coin['offset'] == _offset].copy()
    # 构建文件存储路径
    select_coin_file_path = os.path.join(curve_path, f'{cls.stg_name}_select_coin_{period}_{_offset}.pkl')
    curve_file_path = os.path.join(curve_path, f'{cls.stg_name}_curve_{period}_{_offset}.pkl')
    # 本地存在选币文件
    if os.path.exists(select_coin_file_path):
        # 如果存在文件，只更新最近2个offset的资金曲线
        if _offset not in last_offset_list:
            return pd.DataFrame()
        # 计算最多4个周期的数据
        select_coin = select_coin[select_coin['candle_begin_time'] > run_time - 4 * pd.to_timedelta(period) - pd.Timedelta(hours=utc_offset)]
        local_strategy_select_df = pd.read_pickle(select_coin_file_path)
    else:
        local_strategy_select_df = pd.DataFrame()
    # 本地存在资金数据
    if os.path.exists(curve_file_path):
        # 如果存在文件，只更新最近2个offset的资金曲线
        if _offset not in last_offset_list:
            return pd.DataFrame()
        local_strategy_curve_df = pd.read_pickle(curve_file_path)
        # 容错检查：数据缺失过多，则会发送警告
        if local_strategy_curve_df['candle_begin_time'].max() < run_time - 4 * pd.to_timedelta(period) - pd.Timedelta(hours=utc_offset):
            msg = f'策略【{cls.stg_name}】资金曲线缺少过多，已经超过程序最大容错，程序即将退出。\n建议删除data/curve下所有文件，以及data/账户名称/shift_select_coin_result文件后重试'
            print(msg)
            send_wechat_work_msg(msg, error_webhook_url)
            exit()
        # 重复计算检查：如果资金曲线的数据保留最新，则直接返回
        if local_strategy_curve_df['candle_begin_time'].max() == run_time - pd.Timedelta(hours=(utc_offset+1)):
            return calc_equity_for_pct(local_strategy_curve_df)
    else:
        local_strategy_curve_df = pd.DataFrame()

    # 获取benchmark
    benchmark = get_benchmark(run_time, select_coin)

    # ===遍历原始数据的1h的k线，计算周期内单币种的资金曲线
    import time
    _s_time = time.time()
    if njob == 1 or njob == -1:
        period_list = []
        for index, row in select_coin[['symbol', 'symbol_type']].drop_duplicates().iterrows():
            period_list.append(calc_hour_data(row['symbol'], row['symbol_type'], benchmark, period, _offset))
    else:
        period_list = Parallel(n_jobs=njob)(
            delayed(calc_hour_data)(row['symbol'], row['symbol_type'], benchmark, period, _offset)
            for index, row in select_coin[['symbol', 'symbol_type']].drop_duplicates().iterrows())
    print('calc_hour_data:', time.time() - _s_time)

    if not len(period_list):
        return pd.DataFrame()
    # 合并数据
    all_period_list = pd.concat(period_list, ignore_index=True)
    if period[-1] == 'H':
        select_coin['candle_begin_time'] += pd.to_timedelta('1H')
    else:
        select_coin['candle_begin_time'] += pd.to_timedelta('1D')
    select_coin = pd.merge(select_coin,
                           all_period_list[['candle_begin_time', 'symbol', 'symbol_type', '每小时涨跌幅']],
                           'left', ['candle_begin_time', 'symbol', 'symbol_type'])
    select_coin.loc[select_coin['symbol_type'] == 'spot', 'rate'] = 1 / 1000
    select_coin.loc[select_coin['symbol_type'] == 'swap', 'rate'] = 4 / 10000
    select_coin['lvg'] = 1  # 默认1倍杠杆
    select_coin['选币'] = select_coin['symbol'] + '(' + select_coin['方向'].astype(str) + ')' + ' '
    select_coin = select_coin[['candle_begin_time', 'offset', 'symbol', 'symbol_type', '方向', '每小时涨跌幅', 'rate', 'lvg', '选币']]
    select_coin.dropna(subset=['每小时涨跌幅'], inplace=True)
    select_coin.sort_values(by=['candle_begin_time', 'symbol'], inplace=True)
    select_coin.reset_index(inplace=True, drop=True)

    # 计算多空的资金曲线
    start_time = max(run_time - pd.to_timedelta('365D'), select_coin['candle_begin_time'].min())
    trading_time_list = create_trading_time(benchmark, period, _offset, start_time, run_time)
    long_curve = cal_net_value(select_coin[select_coin['方向'] == 1], period, trading_time_list)
    long_curve['方向'] = 1
    short_curve = cal_net_value(select_coin[select_coin['方向'] == -1], period, trading_time_list)
    short_curve['方向'] = -1

    # 这里排除掉第一个周期的数据，因为第一个周期数据的涨跌幅开平仓比例与回测对应不上
    if not long_curve.empty:
        long_curve = long_curve[long_curve['candle_begin_time'] >= long_curve['candle_begin_time'].min() + pd.to_timedelta(period)]
    if not short_curve.empty:
        short_curve = short_curve[short_curve['candle_begin_time'] >= short_curve['candle_begin_time'].min() + pd.to_timedelta(period)]
    # 将多空方向处理后的数据合并起来
    select_coin = pd.concat([long_curve, short_curve], ignore_index=True)
    # 保存选币周期数据到本地（方便后续计算多空资金曲线）
    select_coin = pd.concat([local_strategy_select_df, select_coin], ignore_index=True)
    select_coin.drop_duplicates(subset=['candle_begin_time', '方向'], keep='last', inplace=True)  # 去重保留最新的数据
    select_coin.sort_values('candle_begin_time', inplace=True)
    select_coin.reset_index(inplace=True, drop=True)
    select_coin.to_pickle(select_coin_file_path)
    # 重新拆分多空资金选币
    long_curve = select_coin[select_coin['方向'] == 1]
    short_curve = select_coin[select_coin['方向'] == -1]

    # ===计算当周期资金曲线
    # 将多空选币的资金曲线合并成当周期内的资金曲线
    group = select_coin.groupby('candle_begin_time')
    # 存储周期内的资金曲线
    merge_df = pd.DataFrame()
    merge_df['选币'] = group['选币'].sum()
    merge_df['offset'] = group['offset'].last()
    # 将多空方向的资金曲线合并起来
    merge_df['每小时资金曲线'] = group['每小时资金曲线'].apply(lambda x: np.array(x).mean(axis=0))  # 计算周期的涨跌幅
    # 计算周期的涨跌幅
    merge_df['周期涨跌幅'] = merge_df['每小时资金曲线'].apply(lambda x: x[-1] - 1)
    # 通过每小时资金曲线，计算每小时账户资金的涨跌幅
    merge_df['每小时涨跌幅'] = merge_df['每小时资金曲线'].apply(lambda x: list(pd.DataFrame([1] + list(x)).pct_change()[0].iloc[1:]))

    if long_curve.empty:
        merge_df['多头周期涨跌幅'] = None
        merge_df['多头每小时涨跌幅'] = None
        merge_df['多头调仓比例'] = None
    else:
        long_curve = long_curve.set_index('candle_begin_time')
        merge_df['多头每小时资金曲线'] = long_curve.loc[:, '每小时资金曲线']
        merge_df['多头周期涨跌幅'] = merge_df['多头每小时资金曲线'].apply(lambda x: x[-1] - 1)
        merge_df['多头每小时涨跌幅'] = merge_df['多头每小时资金曲线'].apply(lambda x: list(pd.DataFrame([1] + list(x)).pct_change()[0].iloc[1:]))
    if short_curve.empty:
        merge_df['空头每小时资金曲线'] = None
        merge_df['空头周期涨跌幅'] = None
        merge_df['空头每小时涨跌幅'] = None
    else:
        short_curve = short_curve.set_index('candle_begin_time')
        merge_df['空头每小时资金曲线'] = short_curve.loc[:, '每小时资金曲线']
        merge_df['空头周期涨跌幅'] = merge_df['空头每小时资金曲线'].apply(lambda x: x[-1] - 1)
        merge_df['空头每小时涨跌幅'] = merge_df['空头每小时资金曲线'].apply(lambda x: list(pd.DataFrame([1] + list(x)).pct_change()[0].iloc[1:]))

    # =====计算小时级别的资金曲线
    # 创造空的时间周期表，用于填充不选币的周期
    empty_df = create_empty_data(benchmark, cls.hold_period, offset=_offset)
    # 填充缺失数据
    empty_df.update(merge_df)
    merge_df = empty_df.copy()
    merge_df['选币'].fillna(method='ffill', inplace=True)
    merge_df.dropna(subset=['选币'], inplace=True)
    # 数据合并
    equity = pd.merge(left=benchmark, right=merge_df[['选币']], on=['candle_begin_time'], how='left', sort=True)
    # 填充选币数据
    equity['选币'].fillna(method='ffill', inplace=True)
    equity.dropna(subset=['选币'], inplace=True)
    # 将每小时涨跌幅数据，填充到index中
    equity['涨跌幅'] = revise_data_length(merge_df['每小时涨跌幅'].sum(), len(equity))
    equity['涨跌幅'].fillna(value=0, inplace=True)
    equity['多头涨跌幅'] = revise_data_length(merge_df['多头每小时涨跌幅'].sum(), len(equity))
    equity['多头涨跌幅'].fillna(value=0, inplace=True)
    equity['空头涨跌幅'] = revise_data_length(merge_df['空头每小时涨跌幅'].sum(), len(equity))
    equity['空头涨跌幅'].fillna(value=0, inplace=True)

    equity = pd.concat([local_strategy_curve_df, equity], ignore_index=True)
    equity.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True)  # 去重保留最新的数据
    equity.sort_values('candle_begin_time', inplace=True)
    equity.reset_index(inplace=True, drop=True)
    equity.to_pickle(curve_file_path)

    return calc_equity_for_pct(equity)


def calc_equity_for_pct(equity):
    """
    计算最终每小时净值变化
    """
    equity['多空资金曲线'] = (equity['涨跌幅'] + 1).cumprod()
    equity['多空资金曲线'].fillna(value=1, inplace=True)
    equity['多头资金曲线'] = (equity['多头涨跌幅'] + 1).cumprod()
    equity['多头资金曲线'].fillna(value=1, inplace=True)
    equity['空头资金曲线'] = (equity['空头涨跌幅'] + 1).cumprod()
    equity['空头资金曲线'].fillna(value=1, inplace=True)

    return equity


def get_benchmark(run_time, df):
    """
    获取benchmark，防止由于数据起点不同，导致resample结果不统一
    :param run_time: 当前运行时间
    :param df: 后续需要合并的数据
    :return:
    """
    start_time = max(run_time - pd.to_timedelta('365D'), df['candle_begin_time'].min())
    end_time = run_time - pd.Timedelta(hours=utc_offset)
    # 定义一个开始的基准时间，避免周期转换出现问题
    time_list = pd.date_range(start='2017-01-01', end=end_time, freq='1H')
    time_list = time_list[(time_list == '2017-01-01') | (time_list > start_time) & (time_list < end_time)]
    benchmark = pd.DataFrame(time_list)
    benchmark.rename(columns={0: 'candle_begin_time'}, inplace=True)

    return benchmark


def calc_hour_data(symbol, symbol_type, benchmark, period, offset):
    """
    计算小时k线数据，主要是周期内的每小时涨跌幅，用于后面计算资金曲线使用
    :param symbol:          币种数据
    :param symbol_type:     币种类型
    :param benchmark:       benchmark
    :param period:          持仓周期
    :param offset:          offset
    :return:
    """
    # =计算的处理数据
    file_path = os.path.join(data_center_path, 'kline', symbol_type, f'{symbol}.csv')

    _df = pd.read_csv(file_path, encoding='gbk', parse_dates=['candle_begin_time'])  # 读取k线数据

    # ===与benchmark合并
    _df = pd.merge(left=benchmark, right=_df, on='candle_begin_time', how='left', sort=True, indicator=True)

    # ===处理原始数据
    _df.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True)  # 去重保留最新的数据
    _df.sort_values('candle_begin_time', inplace=True)  # 通过candle_begin_time排序
    _df['涨跌幅'] = _df['close'].pct_change(1)
    _df['涨跌幅'].fillna(value=_df['close'] / _df['open'] - 1, inplace=True)
    _df['fundingRate'].fillna(value=0, inplace=True)

    _df.set_index('candle_begin_time', inplace=True)
    # 必备字段
    agg_dict = {
        'symbol': 'first',
        '_merge': 'first'
    }
    if period == '1H':
        period_df = _df[agg_dict.keys()]
        period_df['每小时涨跌幅'] = _df['涨跌幅'].transform(lambda x: [x])
        period_df['offset'] = 0
        period_df.reset_index(inplace=True)

        # 删除nan
        period_df.dropna(subset=['symbol'], inplace=True)
        period_df = period_df[period_df['_merge'] == 'both']  # 删除掉周期不全数据，有些币重新上线之后前面的数据周期不全，直接删除
        del period_df['_merge']
    else:
        # ===数据转换
        period_df_list = []
        # 通过持仓周期来计算需要多少个offset，遍历转换每一个offset数据
        period_df = _df.resample(period, base=offset).agg(agg_dict)
        period_df['每小时涨跌幅'] = _df['涨跌幅'].resample(period, base=offset).apply(lambda x: list(x))
        period_df['offset'] = offset
        period_df.reset_index(inplace=True)

        # 删除nan
        period_df.dropna(subset=['symbol'], inplace=True)
        period_df = period_df[period_df['_merge'] == 'both']  # 删除掉周期不全数据，有些币重新上线之后前面的数据周期不全，直接删除
        del period_df['_merge']

        # 数据存放到list中
        period_df_list.append(period_df)

        # ===数据合并
        # 将不同offset的数据，合并到一张表
        period_df = pd.concat(period_df_list, ignore_index=True)

    period_df['symbol_type'] = symbol_type
    period_df.sort_values(by='candle_begin_time', inplace=True)
    period_df.reset_index(drop=True, inplace=True)

    return period_df


def create_trading_time(benchmark, hold_period, offset, start_date, end_date):
    """
    根据配置信息，构建当前offset下完整的交易时间，用于后续填充空缺数据
    :param benchmark: 基本数据
    :param hold_period: 持仓周期
    :param offset: 当前offset
    :param start_date: 回测开始时间
    :param end_date: 回测结束时间
    :return:
    """
    trading_time_list = benchmark.copy()
    trading_time_list.set_index('candle_begin_time', inplace=True)
    trading_time_list = trading_time_list.resample(rule=hold_period, base=offset).last().reset_index()
    trading_time_list = trading_time_list[trading_time_list['candle_begin_time'] >= pd.to_datetime(start_date)]
    trading_time_list = trading_time_list[trading_time_list['candle_begin_time'] < pd.to_datetime(end_date)]

    return trading_time_list


def revise_data_length(data, data_len, value=0):
    """
    校正数据长度
    原数据过长，则进行切片
    原数据果断，则使用0填充
    :param data: 原数据
    :param data_len: 资金曲线的数据长度
    :param value: 填充的数据
    :return: 校正后的数据
    """
    if len(data) >= data_len:
        data = data[-data_len:]
    elif len(data) < data_len:
        data.extend([value] * (data_len - len(data)))

    return data


def create_empty_data(index_data, period, offset=0):
    """
    创建空的周期，用于填充未开仓的数据
    :param index_data:  基准数据
    :param period:      持仓周期
    :param offset:      offset偏移周期
    :return:
    """
    # 根据index的时间数据构建df
    empty_df = index_data[['candle_begin_time']].copy()
    empty_df['涨跌幅'] = 0.0
    empty_df['多头涨跌幅'] = 0.0
    empty_df['空头涨跌幅'] = 0.0

    empty_df.set_index('candle_begin_time', inplace=True)

    # 构建空的周期数据
    empty_period_df = pd.DataFrame()
    empty_period_df['每小时涨跌幅'] = empty_df['涨跌幅'].resample(period, base=offset).apply(lambda x: list(x))
    empty_period_df['多头每小时涨跌幅'] = empty_df['多头涨跌幅'].resample(period, base=offset).apply(lambda x: list(x))
    empty_period_df['空头每小时涨跌幅'] = empty_df['空头涨跌幅'].resample(period, base=offset).apply(lambda x: list(x))
    # 填充其他列
    empty_period_df['offset'] = np.nan
    empty_period_df['选币'] = np.nan
    empty_period_df['每小时资金曲线'] = empty_period_df['每小时涨跌幅'].transform(lambda x: np.ones(len(x)))
    empty_period_df['多头每小时资金曲线'] = empty_period_df['多头每小时涨跌幅'].transform(lambda x: np.ones(len(x)))
    empty_period_df['空头每小时资金曲线'] = empty_period_df['空头每小时涨跌幅'].transform(lambda x: np.ones(len(x)))
    empty_period_df['周期涨跌幅'] = 0.0
    empty_period_df['多头周期涨跌幅'] = 0.0
    empty_period_df['空头周期涨跌幅'] = 0.0

    # 筛选指定字段
    empty_period_df = empty_period_df[['offset', '选币', '每小时资金曲线', '周期涨跌幅', '每小时涨跌幅',
                                       '多头每小时资金曲线', '多头周期涨跌幅', '多头每小时涨跌幅', '空头每小时资金曲线', '空头周期涨跌幅', '空头每小时涨跌幅',
                                       ]]
    return empty_period_df


# 检查数据长度
def check_length(row, col, method='last'):
    """
    有些币会在持仓过程中下架了，这里需要弥补一下数据长度
    :param row:         每一行数据
    :param col:         需要补充的数据长度
    :param method:      补充方式 last: 取最后一个数据进行向后补充  fill0: 使用0进行向后补充
    :return:
    """
    if row['diff'] == 0:
        return row[col]
    if method == 'fill0':
        return np.array(list(row[col]) + [0] * row['diff'])
    if method == 'last':
        return np.array(list(row[col]) + [row[col][-1]] * row['diff'])


def cal_net_value(data, hold_period, trading_time_list):
    """
    根据换仓信息，精确计算手续费及资金费
    :param data:                输入的原始数据
    :param hold_period:         持仓周期
    :param trading_time_list:   交易时间
    :return:
    """
    if data.empty:
        return pd.DataFrame()
    # 合并数据，这里继续做一个替换，整体调仓10%，和每个币调仓10%的效果是一样的。
    data = pd.merge(data, trading_time_list[['candle_begin_time']], 'left', on=['candle_begin_time'])
    # 计算仓位价值（带杠杆的情况下，不能用净值直接计算手续费和资金费）
    data['仓位价值'] = data.apply(lambda row: np.cumprod(np.array(row['每小时涨跌幅']) + 1) * row['lvg'], axis=1)

    # 如果选到下市的币种，会有周期长度对不上的情况，所以要做一下数据检查
    data['交易长度'] = data['每小时涨跌幅'].apply(len)
    data['最大周期长度'] = data.groupby('candle_begin_time')['交易长度'].transform('max')  # 选一个币的时候，存在单币周期不全的问题
    data['周期长度'] = int(pd.to_timedelta(hold_period) / pd.to_timedelta('1H'))  # 资金曲线小时级别，所有除以1h
    data.loc[data['candle_begin_time'] == data['candle_begin_time'].max(), '周期长度'] = data['最大周期长度']  # 修复处理最后一个周期数据长度不全的问题
    data['diff'] = data['周期长度'] - data['交易长度']  # 计算币种缺失的长度。0：表示没有数据缺失，持仓期间币种没有下架， 10：表示币种缺失10小时数据

    # 如果数据差异最大值超过0，表示需要进行数据检查
    if data['diff'].max() > 0:
        data['仓位价值'] = data.apply(lambda rows: check_length(rows, '仓位价值'), axis=1)

    # 判断一下当前的方向
    data['每小时资金曲线'] = data.apply(lambda row: row['仓位价值'] - row['lvg'] + 1 if row['方向'] == 1 else row['lvg'] - row['仓位价值'] + 1, axis=1)
    # 删除不必要的列
    data.drop(columns=['仓位价值'], inplace=True)
    # 重置索引
    data.reset_index(inplace=True)

    # 进行数据合并，将当周期上所有币种的信息进行合并处理
    groups = data.groupby(['candle_begin_time'])
    res_df = pd.DataFrame(groups['选币'].sum())
    res_df['offset'] = groups['offset'].last()
    res_df['每小时资金曲线'] = groups['每小时资金曲线'].apply(lambda x: np.array(x).mean(axis=0))
    res_df.reset_index(inplace=True)

    # 合并完整的时间周期，用于填充空缺的资金曲线
    res_df = pd.merge(left=trading_time_list, right=res_df, on='candle_begin_time', how='left')
    res_df['选币'].fillna(value='空仓', inplace=True)
    res_df['offset'].fillna(method='ffill', inplace=True)
    hours = int(hold_period / pd.to_timedelta('1h'))
    res_df['每小时资金曲线'] = res_df.apply(lambda row: np.ones(hours) if row['选币'] == '空仓' else row['每小时资金曲线'], axis=1)

    return res_df


def transfer_curve_for_shift_cfg(curve_df, run_time, strategy_dict, stg_name, strategy, _offset):
    """
    将资金曲线数据按照子策略的周期进行数据重聚合
    :param curve_df: 资金曲线的路径
    :param run_time: 当前运行时间
    :param strategy_dict: 轮动策略的配置信息
    :param stg_name: 子策略的名称
    :param strategy: 子策略配置信息
    :param _offset: offset
    :return:
    """
    # 获取benchmark
    benchmark = get_benchmark(run_time, curve_df)

    # 如果不是1H持仓周期的，存在不同的open、high、low、close；如果是1H持仓周期，则open=high=low=close
    resample_cols = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'}

    # 创建空列表用来保存整理好的df
    equity_list = []
    # 遍历指定的资金曲线
    for equity_col in strategy_dict[stg_name]:
        # 取出对应的资金曲线数据、对应的选币数据、对应的以close计算的资金曲线数据
        _equity = curve_df[['candle_begin_time', equity_col]]
        # 开始时间
        _start_date = _equity['candle_begin_time'].iloc[0]

        # 把资金曲线列复制成open、high、low、close。为了回测与实盘对应，使用close计算得到的资金曲线为基础，生成ohlc，计算因子
        _equity['open'] = _equity[equity_col]
        _equity['high'] = _equity[equity_col]
        _equity['low'] = _equity[equity_col]
        _equity['close'] = _equity[equity_col]

        # 计算每小时的涨跌幅并resample为list格式，计算涨跌幅仍然使用的是avg_price计算得到的资金曲线
        _equity['涨跌幅'] = _equity[equity_col].pct_change()
        _equity['涨跌幅'].fillna(_equity[equity_col] - 1, inplace=True)

        # ===与benchmark合并
        _equity = pd.merge(left=benchmark, right=_equity, on='candle_begin_time', how='left')
        _equity.sort_values(by='candle_begin_time', inplace=True)

        # resample数据
        period_equity = _equity.resample(strategy.hold_period, on='candle_begin_time', base=_offset).agg(resample_cols)
        period_equity.reset_index(inplace=True)

        # resample涨跌幅为每小时涨跌幅
        period_equity['每小时涨跌幅'] = _equity.resample(strategy.hold_period, on='candle_begin_time', base=_offset)['涨跌幅'].apply(lambda x: list(x)).values

        # 根据开始时间截取之后的数据
        period_equity = period_equity[period_equity['candle_begin_time'] >= _start_date]

        # 指定策略名；格式为 策略名 + 多空/多头/空头 + offset
        period_equity['symbol'] = stg_name + '_eq_' + equity_col + '_eq_' + str(_offset)

        # =把生成好的df添加到列表中
        equity_list.append(period_equity)

    # 合并并整理数据
    equity = pd.concat(equity_list, axis=0)
    equity = equity.sort_values(['candle_begin_time', 'symbol']).reset_index(drop=True)
    equity['symbol_type'] = 'Shift'

    if equity.empty:
        return pd.DataFrame()

    return equity


def calc_strategy_factors(all_equity, symbol, shift_strategy, hold_period, factor_cols):
    """
    计算因子
    :param all_equity:          所有资金曲线的数据
    :param symbol:              资金曲线名
    :param shift_strategy:      轮动策略配置
    :param hold_period:         子策略持仓周期
    :param factor_cols:         需要保存的因子名
    :return:
        返回每个资金曲线计算好因子之后的df
    """
    # 取出对应资金曲线的数据
    df = all_equity[all_equity['symbol'] == symbol].reset_index(drop=True)
    df['len'] = df['每小时涨跌幅'].apply(len)
    df = df[df['len'] == df['len'].max()].reset_index(drop=True)

    # =根据净值数据计算因子
    from utils.functions import cal_factors
    df = cal_factors(df, shift_strategy.factor_list, hold_period=hold_period, save_cols=factor_cols)

    return df


def select_all_offset_strategy(df, shift_strategy):
    """
    选择所有offset的轮动资金曲线
    :param df:                  所有参与轮动策略的资金曲线
    :param shift_strategy:      轮动策略配置
    :return:
        返回各个策略各offset轮动结果（首次是全部offset，后续每次更新最近2各offset）
    """
    result = []
    # 计算每个策略对应的offset
    df['offset'] = df['symbol'].apply(lambda x: x.split('_eq_')[2])
    df['offset'] = df['offset'].astype(int)
    # 遍历offset进行轮动
    for _offset in df['offset'].unique():
        # 指定offset
        _df = df[df['offset'] == _offset].copy()
        # 轮动选策略
        result_df = select_strategy(_df, shift_strategy.factor_list, shift_strategy.select_num)
        # 保存结果
        result.append(result_df)

    return result


def select_strategy(df, factor_list, select_num, **kwargs):
    """
    根据计算出的因子选择策略
    :param df:              所有资金曲线的数据，已经计算好因子
    :param factor_list:     因子列表
    :param select_num:      选策略的数量
    :param kwargs:          扩充字段
    :return:
        返回根据因子选择出来的策略
    """
    # 接受外部回测的因子列表，这里主要是去适配`4_遍历选币参数.py`
    external_list = kwargs.get('external_list', [])
    if external_list:  # 如果存在外部回测因子列表，则使用外部因子列表
        _factor_list = external_list
    else:  # 如过不存在外部回测因子列表，默认使用当前策略的因子列表
        _factor_list = factor_list

    df['_rank'] = 0
    for factor_name, if_reverse, parameter_list, weight in _factor_list:
        col_name = f'{factor_name}_{str(parameter_list)}'
        # 计算单个因子的排名
        df[col_name + '_rank'] = df.groupby('candle_begin_time')[col_name].rank(ascending=if_reverse, method='min')
        # 将因子按照权重累加
        df['_rank'] += (df[col_name + '_rank'] * weight)
    df['rank'] = df.groupby('candle_begin_time')['_rank'].rank(ascending=True, method='first')

    # 选择排名小于select_num的对应的策略
    df = df[df['rank'] <= select_num].reset_index(drop=True)

    return df


def get_select_coin_for_strategy(select_strategy_list, all_select_df, run_time, hold_period, account_info):
    """
    通过轮动的策略反推实际选币
    :param select_strategy_list:    轮动的资金曲线，用于获取实际选币
    :param all_select_df:           所有策略最新一个周期的选币结果
    :param run_time:                当前运行时间
    :param hold_period:             子策略的持仓周期
    :param account_info:            账户信息
    :return:
        返回轮动的选币结果（包含所有offset）
    """
    select_result = {}  # 轮动的选币结果
    for strategy_df in select_strategy_list:
        symbol = strategy_df['symbol'].iloc[-1]  # 轮动的曲线名称：策略 + 曲线名称（多空/多头）+ offset
        offset = strategy_df['offset'].iloc[-1]  # offset
        strategy_name = symbol.split('_eq_')[0]  # 策略名称
        curve_type = symbol.split('_eq_')[1]  # 曲线名称

        # 获取最新一个周期的选币
        last_df = strategy_df[strategy_df['candle_begin_time'] > (run_time - 2 * pd.to_timedelta(hold_period) - pd.Timedelta(hours=utc_offset))]
        if last_df.empty:
            select_result[f'{offset}'] = {
                'status': '空仓' + '_eq_' + '空仓' + '_eq_' + str(offset),
                'select_coin': pd.DataFrame()
            }
            continue

        # 通过策略名反推选币
        _temp = all_select_df[strategy_name]
        cond1 = _temp['offset'] == offset
        if curve_type == '多空资金曲线':
            select_result[f'{offset}'] = {
                'status': symbol,
                'select_coin': _temp[cond1]
            }
        if curve_type == '多头资金曲线':
            select_result[f'{offset}'] = {
                'status': symbol,
                'select_coin': _temp[cond1 & (_temp['方向'] == 1)]
            }
        if curve_type == '空头资金曲线':
            select_result[f'{offset}'] = {
                'status': symbol,
                'select_coin': _temp[cond1 & (_temp['方向'] == -1)]
            }

    # 构建轮动存储选币目录
    file_path = os.path.join(data_path, f'{account_info["account_name"]}/shift_select_coin_result.pkl')
    # 判断是否需要更新本地选币文件
    if os.path.exists(file_path):  # 文件存在，说明需要更新最近的选币文件
        # 读取本地存储的文件
        with open(file_path, 'rb') as f:
            local_select_result = pickle.load(f)
        # 更新选币
        for key in list(local_select_result.keys()):
            if key not in select_result.keys():  # 将本地不需要更新的数据，赋值到select_result
                select_result[key] = local_select_result[key].copy()

    # 保存轮动选币文件
    with open(file_path, 'wb') as f:
        pickle.dump(select_result, f)

    return select_result
